from chimera.core.chimeraobject import ChimeraObject
from chimera.core.event import event
from chimera.core.lock import lock
from chimera.util.catalogs.landolt import Landolt
from chimera.util.coord import Coord
from chimera.util.image import Image
from chimera.util.position import Position
import copy
from datamodel import *
from databackup import *
from dataproxy import *
from dataview import *
from datetime import datetime
import ephem
import os
from sqlalchemy import desc # Used by dataview
from types import NoneType, StringTypes
import urllib

if __name__ == "__main__":
    print "This module is not intended to be run as a script."

class Database(ChimeraObject):
    """
    This class is a wrapper for the Chimera photometry database, used by the
    photometry and extinction coefficient modules.

    The key entities in the database are as follows:
    Star - represents a star (or other object) in the sky. This includes the
        right ascension and declination of the star, as well as a name, a list
        catalogs containing the star, and, optionally, a list of the star's
        apparent magnitudes in different bands.
        * Can be specified using a dataproxy Star or ExposureStar, a photometry
        Star or ExposureStar, an object name string, or a tuple containing the
        RA and Dec (containing the angles in degrees as floats or in HMS and DMS
        respectively as strings).
    Exposure - represents an image taken. This includes the RA and Dec of the
        center of the image, the date and time taken, the local sidereal time,
        altitude and azimuth, and the filter through which the image was taken.
        * Can be specified using a dataproxy Exposure, a photometry Exposure,
        an chimera.util.image Image, or a file name string.
    ExposureStar - represents a star in an image. This points to a Star for RA,
        dec, and star name, and an Exposure for the date and time taken, LST,
        and filter. Also included are the altitude and azimuth of the star, and
        data gathered by photometry, including the flux, FWHM, instrumental
        magnitude, and airmass.
        * Can be specified using a combination of a star specification and an
        exposure specification.
    """
    __config__ = {"database_file": "photometry.db",
                  # Landolt autoload settings
                  "auto_add_landolt" : True,
                  # Auto catalog settings (catalog to store unknown stars)
                  "auto_catalog_enabled" : True,
                  "auto_catalog_name" : "(Auto)",
                  # Simbad autoload settings
                  "auto_add_simbad" : False,
                  "simbad_objtypes" : "*iC,~V*,~**", # Object types to use/filter out
                  "simbad_entries" : "simbad_sources.txt", # Field list file
                  # Camera listening settings
                  "auto_add_on_expose" : False,
                  "camera" : "/Camera/0",
                  # Photometry listening settings
                  "auto_add_on_photometry" : False,
                  "photometry" : "/Photometry/0",
                  # Reduction listening settings
                  "auto_add_on_reduction" : False,
                  "reduction" : "/Reduction/0",
                  # Search area size for getStar (each dimension in arcseconds)
                  "get_star_ra_tolerance" : 12,
                  "get_star_dec_tolerance" : 6
                  }


    def __init__(self):
        ChimeraObject.__init__(self)


    def __start__(self):
        exists = os.access(self["database_file"], os.F_OK)

        connect(self["database_file"])
        
        if not exists:
            create_all()
            
            if self["auto_add_landolt"]:
                landolt = Landolt()
                landolt.useTarget(Position.fromRaDec(0, 0), 180)
                stars = landolt.find()
                self.importCatalog("Landolt", stars)
        
            if self["auto_add_simbad"]:
                # We're taking two passes here
                # The first gets the data we're looking for, including magnitudes,
                #  and stores it in the specified catalog(s).
                # The second pass just gets the star names for all stars around
                #  those same fields and stores them in the "Simbad" catalog.
                objtypes = self["simbad_objtypes"].split(',')
                file = open(self["simbad_entries"], "r")
                for line in file:
                    if line.endswith('\n'): line = line[0:-1]
                    cells = line.split(';')
                    if len(cells) == 1:
                        self.importSimbadField(cells[0], objTypeFilter=objtypes)
                    elif len(cells) == 2:
                        self.importSimbadField(cells[0], radius=int(cells[1]), objTypeFilter=objtypes)
                    elif len(cells) == 3:
                        if cells[2].find(',') >= 0: cat = cells[2].split(',')
                        else:                       cat = cells[2]
                        self.importSimbadField(cells[0], radius=int(cells[1]), catalog=cat, objTypeFilter=objtypes)
                    elif len(cells) == 4:
                        if cells[2].find(',') >= 0: cat = cells[2].split(',')
                        else:                       cat = cells[2]
                        objtypes = cells[3].split(',')
                        self.importSimbadField(cells[0], radius=int(cells[1]), catalog=cat, objTypeFilter=objtypes)
                    self.importSimbadField(cells[0], int(cells[1]) * 1.25, addMagnitudes = False)
                file.close()
                
        self._site = self.getManager().getProxy("/Site/0")
        
        if self["auto_add_on_expose"]:
            self.camera = self.getManager().getProxy(self["camera"])
            if self.camera:
                self.camera.readoutComplete += self.getProxy()._cameraExposed

        #if self["auto_add_on_reduction"]:
        #    self.reduction = self.getManager().getProxy(self["reduction"])
        #    if self.reduction:
        #        self.reduction.reductionComplete += self.getProxy()._reductionComplete

        if self["auto_add_on_photometry"]:
            self.photometry = self.getManager().getProxy(self["photometry"])
            if self.photometry:
                self.photometry.photometryCompleted += self.getProxy()._photometryCompleted

        self.starAdded += self.getProxy()._contentAdded
        self.starUpdated += self.getProxy()._contentUpdated
        self.exposureAdded += self.getProxy()._contentAdded
        self.exposureUpdated += self.getProxy()._contentUpdated
        self.extinctionAdded += self.getProxy()._contentAdded

        return True


    @event
    def starAdded(self, star):
        """Indicates that a star has been added to the database."""

    @event
    def starUpdated(self, star):
        """Indicates that a star in the database has been updated."""

    @event
    def exposureAdded(self, exposure):
        """Indicates that an exposure has been added to the database."""

    @event
    def exposureUpdated(self, exposure):
        """Indicates that an exposure in the database has been updated."""

    @event
    def extinctionAdded(self, extinctionTuple):
        """Indicates that an extinction coefficient has been added to the database."""


    def _cameraExposed(self, image):
        self.addExposure(str(image.filename()))


    def _reductionComplete(self, imagerequest):
        pass


    def _photometryCompleted(self, exposure):
        filename = str(exposure.getFileName())
        if self.containsExposure(filename):
            self.updateExposure(filename, exposure)
        else:
            self.addExposure(exposure)


    def _contentAdded(self, content):
        self.log.info("Content added to database:" + str(content))


    def _contentUpdated(self, content):
        self.log.info("Content updated in database:" + str(content))


    @lock
    def add1stOrderExtinction(self, startTime, endTime, filter, coeff, dataUsed = None):
        """
        Stores a calculated 1st-order extinction coefficient in the database,
        with the applicable date-time range and camera filter.
        """
        if dataUsed == None:
            Extinction(starttime = startTime,
                       endtime = endTime,
                       refband = filter,
                       otherbands = "",
                       coefficient1 = coeff,
                       coefficient2 = 0)
        else:
            Extinction(starttime = startTime,
                       endtime = endTime,
                       refband = filter,
                       otherbands = "",
                       coefficient1 = coeff,
                       coefficient2 = 0,
                       dataused = dataUsed)
        self.extinctionAdded((1, coeff, startTime, endTime, filter))
        session.commit()


    @lock
    def add2ndOrderExtinction(self, startTime, endTime, refBand, otherBands, coeff1, coeff2, dataUsed = None):
        """
        Stores a calculated 1st-order and 2nd-order extinction coefficients in
        the database, with the applicable date-time range and camera filters.
        """
        if dataUsed == None:
            Extinction(starttime = startTime,
                       endtime = endTime,
                       refband = refBand,
                       otherbands = ",".join(otherBands),
                       coefficient1 = coeff1,
                       coefficient2 = coeff2)
        else:
            Extinction(starttime = startTime,
                       endtime = endTime,
                       refband = refBand,
                       otherbands = ",".join(otherBands),
                       coefficient1 = coeff1,
                       coefficient2 = coeff2,
                       dataused = dataUsed)
        self.extinctionAdded((2, coeff1, coeff2, startTime, endTime, refBand, otherBands))
        session.commit()


    @lock
    def addExposure(self, newExposure):
        """
        Stores an exposure in the database, with any exposure-stars attached to
        it.
        """
        if self.containsExposure(newExposure):
            raise Exception("Attempted to add an exposure that is already in the database.")

        if isinstance(newExposure, Image):
            image = newExposure
            filename = image.filename
        elif isinstance(newExposure, StringTypes):
            image = Image.fromFile(newExposure)
            filename = newExposure
        else:
            try:
                image = Image.fromFile(newExposure.getFileName())
                filename = newExposure.getFileName()
            except AttributeError, ex:
                raise ValueError("Data cannot be passed in using this type of object.\n" + str(ex))

        # Get latitude from FITS header or site information
        if 'LATITUDE' in image:  latitude = Coord.fromDMS(image['LATITUDE'])
        elif 'SITELAT' in image: latitude = Coord.fromD(image['SITELAT'])
        elif self._site != None: latitude = self._site["latitude"]
        else:                    latitude = Coord.fromD(0)

        # Get longitude from FITS header or site information
        if 'LONGITUD' in image:   longitude = Coord.fromDMS(image['LONGITUD'])
        elif 'SITELONG' in image: longitude = Coord.fromD(image['SITELONG'])
        elif self._site != None:  longitude = self._site["longitude"]
        else:                     longitude = Coord.fromD(0)

        # Get observation date and time from FITS header
        try:        obsdate = datetime.strptime(image['DATE-OBS'], '%Y-%m-%dT%H:%M:%S')
        except:
            try:    obsdate = datetime.strptime(image['DATE'], '%Y-%m-%dT%H:%M:%S')
            except: obsdate = datetime.now()


        # Calculate local sidereal time
        obs = ephem.Observer()
        obs.lat = latitude.strfcoord('%(d)d:%(m)d:%(s).2f')
        obs.long = longitude.strfcoord('%(d)d:%(m)d:%(s).2f')
        obs.pressure = 0
        obs.date = obsdate
        lst = Coord.fromHMS(str(obs.sidereal_time()))

        # Get RA and Dec from FITS header
        ra = Coord.fromD(image['CRVAL1']); dec = Coord.fromD(image['CRVAL2'])

        # Get altitude and azimuth from FITS header or recalculate
        if ('ALT' in image) and ('AZ' in image):
            alt = Coord.fromDMS(image['ALT']); az = Coord.fromDMS(image['AZ'])
        else:
            raDec = Position((ra, dec))
            altAz = Position.raDecToAltAz(raDec, latitude, lst)
            alt = altAz.alt; az = altAz.az

        # Get image type from FITS header, if available
        if 'IMAGETYP' in image: imagetype = image['IMAGETYP']
        else:                   imagetype = ""

        # Get exposure time from FITS header, if available
        if 'EXPTIME' in image: exptime = float(image['EXPTIME'])
        else:                  exptime = 0.0

        # Get filter from FITS header, if available
        if 'FILTER' in image:    filter = image['FILTER']
        elif 'FILTERS' in image: filter = image['FILTERS']
        else:                    filter = ""

        # Get observer from FITS header, if available
        if 'OBSERVER' in image: observer = image['OBSERVER']
        else:                   observer = ""

        exp = Exposure(filename = filename,
                       ra = ra.D,
                       dec = dec.D,
                       alt = alt.D,
                       az = az.D,
                       lst = lst.D,
                       latitude = latitude.D,
                       longitude = longitude.D,
                       obsdate = obsdate,
                       imagetype = imagetype,
                       exptime = exptime,
                       filter = filter,
                       observer = observer)

        self.exposureAdded(exp)
        self._addExposureNote(exp, name = "Storage")

        try: self._addExposureStars(exp, newExposure.getAllStars())
        except Exception, e: self.log.debug("Cannot add exposure stars: %s\n%s" % (exp, e))

        session.commit()


    @lock
    def addExposureNote(self, exposure, name, detail = "", date = None):
        """
        Adds an item to the list of notes done to an exposure. By default, the
        detail text is empty and the date is set to the current date and time.
        """
        self._addExposureNote(exposure, name, detail, date)
        session.commit()
    def _addExposureNote(self, exposure, name, detail = "", date = None):
        if date == None: date = datetime.now()
        exposure = self._getExposure(exposure)
        ExposureTask(exposure = exposure,
                     name = name,
                     detail = detail,
                     date = date)


    def _addExposureStars(self, exposure, expstars):
        # NOTE: Currently, this method assumes that the first exposure-star to
        #  match a given star is the correct exposure-star for the match. A more
        #  accurate algorithm to determine the best match could and probably
        #  should be added later.

        # Delete existing exposure-stars
        for expstar in exposure.stars: expstar.delete()

        # Add exposure-stars from the data
        exclude = []
        for expstar in expstars:
            star = self._getStar(expstar, add = True, exclude = exclude)
            try:
                ExposureStar(exposure = exposure,
                             star = star,
                             photra = expstar.getRA().D,
                             photdec = expstar.getDec().D,
                             alt = expstar.getAlt().D,
                             az = expstar.getAz().D,
                             fwhm = expstar.getFWHM(),
                             flux = expstar.getFlux(),
                             magnitude = expstar.getMagnitude(),
                             airmass = expstar.getAirmass())
            except Exception, e: raise Exception("Failed to add exposure star %s to %s\n%s" % (expstar, exposure, e))
            exclude.append(star)

        self._addExposureNote(exposure, name = "Photometry")


    @lock
    def addStar(self, star):
        """
        Stores a general star object. Requires a photometry Star or ExposureStar
        object.
        """
        if self.containsStar(star):
            raise Exception("Failed to add star %s\nStar already in the database." % (star))
        else:
            try:
                newStar = Star(ra = newStar.getRA().D,
                               dec = newStar.getDec().D,
                               objname = newStar.getObjName())
            except Exception, e: raise Exception("Failed to add star %s\n%s" % (star, e))
            session.commit()

            self.starAdded(newStar)


    @lock
    def addStarMagnitude(self, star, filter, magnitude, source = "(Auto)"):
        """
        Adds an apparent magnitude in a band to a star, with the source
        optionally specified.
        """
        star = self._getStar(star)
        catalog = Catalog.query.filter_by(name = source).one()
        StarMagnitude(star = star,
                      source = catalog,
                      filter = filter,
                      magnitude = magnitude)
        session.commit()


    @lock
    def addStarRADec(self, ra, dec, objname = None):
        """
        Stores a general star object with each parameter supplied manually.
        """
        if isinstance(ra, StringTypes):
            ra = Coord.fromHMS(ra).D
        if isinstance(dec, StringTypes):
            dec = Coord.fromDMS(dec).D

        if self.containsStar((ra, dec)):
            raise Exception("Failed to add star %s\nStar already in the database." % ((ra, dec, objname)))
        else:
            newStar = Star(ra = ra, dec = dec, objname = objname)
            session.commit()
            self.starAdded(newStar)


    @lock
    def addStarToCatalog(self, star, catalog):
        """
        Attaches a general star object to a star catalog, creating the catalog
        if necessary.
        """
        star = self._getStar(star)
        catalog = self._getCatalog(catalog, add=True)
        if catalog not in star.catalogs:
            star.catalogs.append(catalog)
        session.commit()
        self.starUpdated(star)


    @lock
    def backup(self, filename):
        """
        Saves a backup of all the database contents to a file.
        """
        backup(self, filename)


    @lock
    def containsCatalog(self, catalog):
        """
        Checks if the database contains the specified catalog.
        """
        try: self._getCatalog(catalog, add = False); return True
        except:                                      return False


    @lock
    def containsExposure(self, exposure):
        """
        Checks if the database contains the specified exposure.
        """
        try: self._getExposure(exposure); return True
        except:                           return False


    @lock
    def containsStar(self, star):
        """
        Checks if the database contains the specified star.
        """
        try: self._getStar(star); return True
        except:                   return False


    @lock
    def get1stOrderExtinction(self, obsdate, filter = None):
        """
        Returns the previously calculated extinction coefficient for the specified
        date, time, and filter, or None if no value is available.
        """
        q = Extinction.query.filter(Extinction.starttime < obsdate)
        q = q.filter(Extinction.endtime > obsdate)
        if filter != None: q = q.filter_by(refband = filter)
        q = q.filter_by(coefficient2 = 0)
        ex = q.one()
        if ex != None: return float(ex.coefficient1)
        else:          return None


    @lock
    def get2ndOrderExtinction(self, obsdate):
        """
        Returns the previously calculated extinction coefficients (in a tuple)
        for the specified date and time, or None if no value is available.
        """
        q = Extinction.query.filter(Extinction.starttime < obsdate)
        q = q.filter(Extinction.endtime > obsdate)
        q = q.filter(Extinction.coefficient2 != 0)
        ex = q.one()
        if ex != None: return (float(ex.coefficient1), float(ex.coefficient2))
        else:          return None


    def _getAutoCatalog(self):
        return self._getCatalog(self.getAutoCatalogName(), add=True)


    def getAutoCatalogName(self):
        return self["auto_catalog_name"]


    @lock
    def getAutoCatalogStars(self, onlyWithData = False):
        return self.getCatalogStars(self.getAutoCatalogName(), onlyWithData)


    def _getCatalog(self, catalogName, add=False):
        try:        return Catalog.query.filter_by(name = catalogName).one()
        except:
            if add: return Catalog(name = catalogName)
            else:   raise Exception("Catalog not found: %s" % (catalogName))


    @lock
    def getCatalogNames(self):
        """
        Returns a list of catalog names.
        """
        return [str(c.name) for c in Catalog.query.all()]


    @lock
    def getExposure(self, exposure):
        """
        Returns the specified exposure.
        """
        return DbExposure(self._getExposure(exposure))
    def _getExposure(self, exposure):
        try:
            if isinstance(exposure, StringTypes):
                return Exposure.query.filter_by(filename = exposure).one()
            elif isinstance(exposure, Image):
                return Exposure.query.filter_by(filename = exposure.compressedFilename())
            elif isinstance(exposure, DbExposure):
                return Exposure.query.filter_by(filename = exposure.filename).one()
            elif isinstance(exposure, Exposure):
                return exposure
        except:
            raise Exception("Exposure not found: %s" % (str(exposure)))
        raise Exception("Exposure not found: %s" % (str(exposure)))


    @lock
    def getExposureCount(self):
        """
        Returns the number of exposures currently in the database.
        """
        return len(Exposure.query.all())


    @lock
    def getExposures(self, minRA = None, maxRA = None, minDec = None,
            maxDec = None, minAlt = None, maxAlt = None, minAz = None,
            maxAz = None, filter = None, expTime = None, imageType = None,
            startObsDate = None, endObsDate = None, observer = None, note = None):
        """
        Returns a list of exposure objects, optionally filtering by camera
        filter, exposure time, and a date range for when the exposure was taken.
        """
        exps = self._getExposures(minRA, maxRA, minDec, maxDec, minAlt,
                maxAlt, minAz, maxAz, filter, expTime, imageType, startObsDate,
                endObsDate, observer, note)
        return copy.deepcopy([DbExposure(e) for e in exps])
    def _getExposures(self, minRA = None, maxRA = None, minDec = None,
            maxDec = None, minAlt = None, maxAlt = None, minAz = None,
            maxAz = None, filter = None, expTime = None, imageType = None,
            startObsDate = None, endObsDate = None, observer = None, note = None):
        # Initialize query
        q = Exposure.query

        # Filter by RA and Dec, if specified
        if minRA != None:  q = q.filter(Exposure.ra >= minRA)
        if maxRA != None:  q = q.filter(Exposure.ra <= maxRA)
        if minDec != None: q = q.filter(Exposure.dec >= minDec)
        if maxDec != None: q = q.filter(Exposure.dec <= maxDec)

        # Filter by Alt and Az, if specified
        if minAlt != None: q = q.filter(Exposure.alt >= minAlt)
        if maxAlt != None: q = q.filter(Exposure.alt <= maxAlt)
        if minAz != None:  q = q.filter(Exposure.az >= minAz)
        if maxAz != None:  q = q.filter(Exposure.az <= maxAz)

        # Filter by filter, if specified
        if filter != None:
            if filter.startswith('~'): q = q.filter(Exposure.filter != filter[1:])
            else:                      q = q.filter_by(filter = filter)

        # Filter by exposure time, if specified
        if expTime != None: q = q.filter_by(exptime = expTime)

        # Filter by image type, if specified
        if imageType != None:
            if imageType.startswith('~'): q = q.filter(Exposure.imagetype != imageType[1:])
            else:                         q = q.filter_by(imagetype = imageType)

        # Filter by observation date, if range specified
        if startObsDate != None and endObsDate != None:
            q = q.filter(Exposure.obsdate > startObsDate)
            q = q.filter(Exposure.obsdate < endObsDate)

        # Filter by observer, if specified
        if observer != None:
            if observer.startswith('~'): q = q.filter(Exposure.observer != observer[1:])
            else:                        q = q.filter_by(observer = observer)

        # Filter by notes, if specified
        if note != None:
            if note.startswith('~'):      q = q.filter(~Exposure.tasks.any(name = note[1:]))
            elif note.find(',') >= 0:
                for n in note.split(','): q = q.filter(Exposure.tasks.any(name = n))
            else:                         q = q.filter(Exposure.tasks.any(name = note))

        # Return results
        return q.all()


    @lock
    def getExposureStar(self, exposure, star):
        """
        Returns an specified exposure-specific star object.
        """
        return DbExposureStar(self._getExposureStar(exposure, star))
    def _getExposureStar(self, exposure, star):
        star = self._getStar(star)
        exposure = self._getExposure(exposure)
        for expstar in exposure.stars:
            if expstar.star == star: return expstar
        raise Exception("Exposure star not found.")


    @lock
    def getExposureStarCount(self, exposure = None):
        """
        Returns the number of exposure-specific star objects currently in the
        database.
        """
        if exposure == None: return len(ExposureStar.query.all())
        else:                return len(self._getExposure(exposure).stars)


    @lock
    def getExposureStars(self, exposure, minRA = None, maxRA = None,
            minDec = None, maxDec = None, minAlt = None, maxAlt = None,
            minAz = None, maxAz = None, minMag = None, maxMag = None,
            minFlux = None, maxFlux = None, minFWHM = None, maxFWHM = None,
            minAM = None, maxAM = None):
        """
        Returns a list of exposure-specific star objects given an exposure object,
        optionally filtering by ra, dec, altitude, azimuth, magnitude, or flux.
        """
        expstars = self._getExposureStars(exposure, minRA, maxRA, minDec,
                maxDec, minAlt, maxAlt, minAz, maxAz, minMag, maxMag, minFlux,
                maxFlux, minFWHM, maxFWHM, minAM, maxAM)
        return copy.deepcopy([DbExposureStar(s) for s in expstars])
    def _getExposureStars(self, exposure, minRA = None, maxRA = None,
            minDec = None, maxDec = None, minAlt = None, maxAlt = None,
            minAz = None, maxAz = None, minMag = None, maxMag = None,
            minFlux = None, maxFlux = None, minFWHM = None, maxFWHM = None,
            minAM = None, maxAM = None):
        # Initialize query
        exposure = self._getExposure(exposure)
        q = ExposureStar.query.filter_by(exposure = exposure)
        q = q.join(ExposureStar.star)

        # Filter by RA and Dec, if specified
        if minRA != None:  q = q.filter(Star.ra >= minRA)
        if maxRA != None:  q = q.filter(Star.ra <= maxRA)
        if minDec != None: q = q.filter(Star.dec >= minDec)
        if maxDec != None: q = q.filter(Star.dec <= maxDec)

        # Filter by Alt and Az, if specified
        if minAlt != None: q = q.filter(ExposureStar.alt >= minAlt)
        if maxAlt != None: q = q.filter(ExposureStar.alt <= maxAlt)
        if minAz != None:  q = q.filter(ExposureStar.az >= minAz)
        if maxAz != None:  q = q.filter(ExposureStar.az <= maxAz)

        # Filter by magnitude
        if minMag != None: q = q.filter(ExposureStar.magnitude >= minMag)
        if maxMag != None: q = q.filter(ExposureStar.magnitude <= maxMag)

        # Filter by flux
        if minFlux != None: q = q.filter(ExposureStar.flux >= minFlux)
        if maxFlux != None: q = q.filter(ExposureStar.flux <= maxFlux)

        # Filter by FWHM
        if minFWHM != None: q = q.filter(ExposureStar.fwhm >= minFWHM)
        if maxFWHM != None: q = q.filter(ExposureStar.fwhm <= maxFWHM)

        # Filter by airmass
        if minAM != None: q = q.filter(ExposureStar.airmass >= minAM)
        if maxAM != None: q = q.filter(ExposureStar.airmass <= maxAM)

        # Get all results and return them
        expstars = q.all()
        return expstars


    @lock
    def getStar(self, star, add = False, exclude = None):
        """
        Finds the specified star in the database, or optionally adds it to the
        database if one was not found within a 2 arcsecond radius (when
        searching by RA/Dec).

        exclude is a list of stars that this star is known not to be.
        """
        result = DbStar(self._getStar(star, add, exclude))
        session.commit()
        return result
    def _getStar(self, star, add = False, exclude = None):
        if isinstance(star, StringTypes):
            try:
                return Star.query.filter_by(objname = star).one()
            except Exception, e:
                raise Exception("Star not found: %s\n%s" % (star, e))
        elif isinstance(star, Star):
            return star
        elif isinstance(star, ExposureStar):
            return star.star
        elif isinstance(star, tuple) or isinstance(star, list):
            # star is a tuple of floats (in decimal degrees) or strings
            #  (RA in HMS, Dec in DMS)
            if len(star) == 2:
                if isinstance(star[0], StringTypes) and isinstance(star[1], StringTypes):
                    ra = Coord.fromHMS(star[0]).D; dec = Coord.fromDMS(star[1]).D
                else:
                    ra = float(star[0]); dec = float(star[1])
            else: raise ValueError("Data must be passed in using one of the accepted formats.")
        else:
            # star is either a photometry.Star, a photometry.ExposureStar,
            #  a dataproxy.DbStar, or a dataproxy.DbExposureStar
            # Let the exception be thrown otherwise
            try: ra = star.getRA().D; dec = star.getDec().D
            except: ValueError("Data must be passed in using one of the accepted formats.")

        if exclude == None: exclude = []
        else:               exclude = [self._getStar(s) for s in exclude]

        closestDistance = 360
        closestStar = None

        raTol = self["get_star_ra_tolerance"] * 0.000277778
        decTol = self["get_star_dec_tolerance"] * 0.000277778
        nearbyStars = self._getStars(minRA = ra - raTol, maxRA = ra + raTol,
                                     minDec = dec - decTol, maxDec = dec + decTol)

        for nearbyStar in nearbyStars:
            # If star not in exclusion list
            if nearbyStar not in exclude:
                # Get distance to the star
                distance = nearbyStar.getDistanceFromRADec(ra, dec)

                # If this is closer than the previous closest, overwrite closest
                if distance < closestDistance:
                    closestDistance = distance
                    closestStar = nearbyStar

        if closestStar != None:
            return closestStar
        if add:
            try:
                newStar = Star(ra = star.getRA().D,
                               dec = star.getDec().D,
                               objname = star.getObjName())

                if self["auto_catalog_enabled"]:
                    newStar.catalogs.append(self._getAutoCatalog())

                #self.starAdded(newStar)
                return newStar
            except Exception, e: raise Exception("Failed to automatically add star %s\n%s" % (star, e))
        else: raise Exception("Star not found: %s" % (star))


    @lock
    def getStarCount(self):
        """
        Returns the number of stars currently in the database.
        """
        return len(Star.query.all())


    @lock
    def getStarExposures(self, star, filter = None, expTime = None,
            startObsDate = None, endObsDate = None, observer = None,
            minAlt = None, maxAlt = None, minAz = None, maxAz = None,
            minFWHM = None, maxFWHM = None, minFlux = None, maxFlux = None,
            minMag = None, maxMag = None, minAM = None, maxAM = None):
        """
        Returns a list of exposure-specific star objects given a general star object,
        optionally filtering by camera filter, exposure time, and a date range for
        when the exposure was taken.
        """
        expstars = self._getStarExposures(star, filter, expTime,
                startObsDate, endObsDate, observer, minAlt, maxAlt, minAz,
                maxAz, minFWHM, maxFWHM, minFlux, maxFlux, minMag, maxMag,
                minAM, maxAM)
        return copy.deepcopy([DbExposureStar(s) for s in expstars])
    def _getStarExposures(self, star, filter = None, expTime = None,
            startObsDate = None, endObsDate = None, observer = None,
            minAlt = None, maxAlt = None, minAz = None, maxAz = None,
            minFWHM = None, maxFWHM = None, minFlux = None, maxFlux = None,
            minMag = None, maxMag = None, minAM = None, maxAM = None):
        # Query star from database
        star = self._getStar(star)

        # Start exposure-star query
        q = ExposureStar.query
        q = q.join(ExposureStar.exposure)
        
        # Filter by star
        q = q.filter(ExposureStar.star == star)

        # Filter by filter, if specified
        if filter != None:
            if filter.startswith('~'): q = q.filter(Exposure.filter != filter[1:])
            else:                      q = q.filter(Exposure.filter == filter)

        # Filter by exposure time, if specified
        if expTime != None: q = q.filter(Exposure.exptime == expTime)

        # Filter by observation date, if range specified
        if startObsDate != None and endObsDate != None:
            q = q.filter(Exposure.obsdate > startObsDate)
            q = q.filter(Exposure.obsdate < endObsDate)

        # Filter by observer, if specified
        if observer != None:
            if observer.startswith('~'): q = q.filter(Exposure.observer != observer[1:])
            else:                        q = q.filter(Exposure.observer == observer)

        # Filter by Alt and Az, if specified
        if minAlt != None: q = q.filter(ExposureStar.alt >= minAlt)
        if maxAlt != None: q = q.filter(ExposureStar.alt <= maxAlt)
        if minAz != None:  q = q.filter(ExposureStar.az >= minAz)
        if maxAz != None:  q = q.filter(ExposureStar.az <= maxAz)

        # Filter by magnitude
        if minMag != None: q = q.filter(ExposureStar.magnitude >= minMag)
        if maxMag != None: q = q.filter(ExposureStar.magnitude <= maxMag)

        # Filter by flux
        if minFlux != None: q = q.filter(ExposureStar.flux >= minFlux)
        if maxFlux != None: q = q.filter(ExposureStar.flux <= maxFlux)

        # Filter by FWHM
        if minFWHM != None: q = q.filter(ExposureStar.fwhm >= minFWHM)
        if maxFWHM != None: q = q.filter(ExposureStar.fwhm <= maxFWHM)

        # Filter by airmass
        if minAM != None: q = q.filter(ExposureStar.airmass >= minAM)
        if maxAM != None: q = q.filter(ExposureStar.airmass <= maxAM)

        # Get all results and return them
        expstars = q.all()
        return expstars


    @lock
    def getStarName(self, star):
        """
        Returns the stored name of a star.
        """
        return str(self._getStar(star).objname)


    @lock
    def getStarMagnitude(self, star, filter = "V", source = None):
        """
        Returns the stored apparent magnitude of a star in the specified band.
        """
        # Query star from database
        star = self._getStar(star)

        # Start exposure-star query
        q = StarMagnitude.query

        # Filter by star
        q = q.filter_by(star = star)

        # Filter by filter, if specified
        if filter != None: q = q.filter(StarMagnitude.filter == filter)

        # Filter by source, if specified
        if source != None: q = q.filter(StarMagnitude.source.has(name = source))

        # Get all results
        magnitudes = q.all()

        # Return the star's apparent magnitude
        if len(magnitudes) > 0: return float(magnitudes[0].magnitude)
        else:                   return None


    @lock
    def getStarMagnitudes(self, star, source = None):
        """
        Returns stored apparent magnitudes of a star for all bands.
        """
        # Query star from database
        star = self._getStar(star)

        # Get magnitudes for the star
        magnitudes = {}
        for mag in star.magnitudes:
            if source == None:
                magnitudes[str(mag.filter)] = float(mag.magnitude)
            elif str(mag.source.name) == source:
                magnitudes[str(mag.filter)] = float(mag.magnitude)

        # Return magnitudes
        return magnitudes


    @lock
    def getStars(self, name = None, minRA = None, maxRA = None, minDec = None, maxDec = None,
        catalog = None, magFilter = None, minMag = None, maxMag = None, hasExposures = None):
        """
        Returns a list of stars contained in the database, optionally filtered.
        """
        stars = self._getStars(name, minRA, maxRA, minDec, maxDec, catalog,
                                 magFilter, minMag, maxMag, hasExposures)
        return copy.deepcopy([DbStar(s) for s in stars])
    def _getStars(self, name = None, minRA = None, maxRA = None, minDec = None, maxDec = None,
        catalog = None, magFilter = None, minMag = None, maxMag = None, hasExposures = None):
        # Initialize query
        q = Star.query

        # Filter by name, if specified
        if name != None: q = q.filter_by(objname = name)

        # Filter by RA and Dec, if specified
        if minRA != None:  q = q.filter(Star.ra >= minRA)
        if maxRA != None:  q = q.filter(Star.ra <= maxRA)
        if minDec != None: q = q.filter(Star.dec >= minDec)
        if maxDec != None: q = q.filter(Star.dec <= maxDec)

        # Filter by catalog, if specified
        if catalog != None:
            if catalog.startswith('~'):        q = q.filter(~Star.catalogs.any(name = catalog[1:]))
            elif catalog.find(',') >= 0:
                for cat in catalog.split(','): q = q.filter(Star.catalogs.any(name = cat))
            else:                              q = q.filter(Star.catalogs.any(name = catalog))

        # Filter by magnitude filter
        if magFilter != None:
            if magFilter.startswith('~'):      q = q.filter(~Star.magnitudes.any(filter = magFilter[1:]))
            elif magFilter.find(',') >= 0:
                for f in magFilter.split(','): q = q.filter(Star.magnitudes.any(filter = f))
            else:                              q = q.filter(Star.magnitudes.any(filter = magFilter))

        # Filter by magnitude:
        if minMag != None: q = q.filter(~Star.magnitudes.any(StarMagnitude.magnitude < minMag))
        if maxMag != None: q = q.filter(~Star.magnitudes.any(StarMagnitude.magnitude > maxMag))

        # Filter by whether or not it has exposures
        if hasExposures != None:
            if hasExposures: q = q.filter(Star.exposures.any())
            else:            q = q.filter(~Star.exposures.any())

        # Return results
        return q.all()


    def getViewer(self):
        """
        Returns an object that can be used to produce tables and plots of the
        data in the database.
        """
        viewer = DataView(self)
        daemon = self.getDaemon()
        if daemon: daemon.connect(viewer); return viewer.getProxy()
        else:                              return viewer


    @lock
    def importCatalog(self, catalogName, catalogData = None):
        """
        Imports a catalog into the database, with all the stars it contains.
        catalogName is a string that will be used to identify the catalog in the
        database, and catalogData is the star data from the catalog as returned
        by VizQuery.find()
        """
        catalog = self._getCatalog(catalogName, add=True)

        if catalogData != None:
            for star in catalogData:
                dataStar = Star(ra = star["RA"].D,
                                dec = star["DEC"].D,
                                objname = star["ID"])

                if "V" in star:
                    dataStar.magnitudes.append(StarMagnitude(filter = "V", magnitude = star["V"], source = catalog))

                if ("B" in star):
                    dataStar.magnitudes.append(StarMagnitude(filter = "B", magnitude = float(star["B"])), source = catalog)
                elif ("V" in star) and ("B-V" in star):
                    dataStar.magnitudes.append(StarMagnitude(filter = "B", magnitude = float(star["B-V"]) + float(star["V"]), source = catalog))

                if ("U" in star):
                    dataStar.magnitudes.append(StarMagnitude(filter = "U", magnitude = float(star["U"])), source = catalog)
                elif ("V" in star) and ("B-V" in star) and ("U-B" in star):
                    dataStar.magnitudes.append(StarMagnitude(filter = "U", magnitude = float(star["U-B"]) + float(star["B-V"]) + float(star["V"]), source = catalog))

                if ("R" in star):
                    dataStar.magnitudes.append(StarMagnitude(filter = "R", magnitude = float(star["R"])), source = catalog)
                elif ("V" in star) and ("V-R" in star):
                    dataStar.magnitudes.append(StarMagnitude(filter = "R", magnitude = float(star["V"]) - float(star["V-R"]), source = catalog))

                if ("I" in star):
                    dataStar.magnitudes.append(StarMagnitude(filter = "I", magnitude = float(star["I"])), source = catalog)
                elif ("V" in star) and ("V-I" in star):
                    dataStar.magnitudes.append(StarMagnitude(filter = "I", magnitude = float(star["V"]) - float(star["V-I"]), source = catalog))
                elif ("V" in star) and ("V-R" in star) and ("R-I" in star):
                    dataStar.magnitudes.append(StarMagnitude(filter = "I", magnitude = float(star["V"]) - float(star["V-R"]) - float(star["R-I"]), source = catalog))

                dataStar.catalogs.append(catalog)

                self.starAdded(dataStar)
        session.commit()


    @lock
    def importSimbadField(self, fieldname, radius = 10, objTypeFilter = ("*", "~Cl*", "~As*"), catalog="Simbad", addMagnitudes=True):
        """
        Queries Simbad for all the stars within a given radius (in arcminutes)
        of a specified object and adds them to the database under a specified
        catalog name (or list of names).
        """
        if isinstance(catalog, tuple) or isinstance(catalog, list):
            catalog = [self._getCatalog(c, add=True) for c in catalog]
        else:
            catalog = self._getCatalog(catalog, add=True)
        simbadCat = self._getCatalog("Simbad", add=True)

        if isinstance(fieldname, StringTypes):
            script = """
                     output console=off script=off
                     format object "\\n%%OTYPELIST\\n%%IDLIST(1)\\n%%COO(:;A),%%COO(:;D)\\n%%FLUXLIST(;N,F,)"
                     query ~%s radius=%dm
                     """
            script = script % (fieldname, radius)
        elif isinstance(fieldname, list) or isinstance(fieldname, tuple):
            if not isinstance(fieldname[0], StringTypes):
                ra = str(Coord.fromD(fieldname[0]).toHMS())
                dec = str(Coord.fromD(fieldname[1]).toDMS())
            elif fieldname[0].find(':') < 0:
                ra = str(Coord.fromD(float(fieldname[0])).toHMS())
                dec = str(Coord.fromD(float(fieldname[1])).toDMS())
            else:
                ra = fieldname[0]
                dec = fieldname[1]
            script = """
                     output console=off script=off
                     format object "\\n%%OTYPELIST\\n%%IDLIST(1)\\n%%COO(:;A),%%COO(:;D)\\n%%FLUXLIST(;N,F,)"
                     query coo %s %s radius=%dm
                     """
            script = script % (ra, dec, radius)
        self.log.debug("Sending script...\n" + script)
        script = urllib.quote_plus(script)

        results = urllib.urlopen("http://simbad.u-strasbg.fr/simbad/sim-script?script=" + script)

        star = None; index = 0; isStar = False
        for line in results:
            if line.startswith("::error::"): raise Exception("Simbad exception.")
            if line.endswith('\n'): line = line[0:-1]
            if len(line) == 0: star = None; index = 0
            elif index == 0:
                if any([((s.startswith('~') and line.find(s[1:]) >= 0) or
                         (not s.startswith('~') and line.find(s) < 0)) for s in objTypeFilter]):
                      isStar = False
                else: isStar = True
                index += 1
            elif isStar:
                fields = line.split(',')
                if index == 1:
                    if self.containsStar(line):
                        star = self._getStar(line)
                    else:
                        star = Star(objname = line)
                    
                    if isinstance(catalog, list):
                        for cat in catalog:
                            if cat not in star.catalogs:
                                star.catalogs.append(cat)
                    elif catalog not in star.catalogs:
                        star.catalogs.append(catalog)
                elif index == 2:
                    star.ra = Coord.fromHMS(fields[0]).D
                    star.dec = Coord.fromDMS(fields[1]).D
                elif index == 3:
                    if addMagnitudes:
                        filter = ""
                        for value in fields:
                            if filter != "":
                                StarMagnitude(star = star,
                                              filter = filter,
                                              magnitude = float(value),
                                              source = simbadCat)
                                filter = ""
                            else: filter = value
                index += 1
        session.commit()


    @lock
    def isStarInCatalog(self, star, catalog):
        """
        Returns a boolean indicating whether or not the specified star is in the
        specified catalog ("Landolt", for example).
        """
        star = self._getStar(star)
        catalog = self._getCatalog(catalog)
        return (star in catalog.stars)


    @lock
    def nameCatalog(self, catalog, newName):
        """
        Renames a catalog in the database.
        """
        self._getCatalog(catalog).name = newName
        session.commit()


    @lock
    def nameStar(self, star, name):
        """
        Assigns a name to an existing star in the database.
        """
        star = self._getStar(star)
        star.objname = name
        session.commit()
        self.starUpdated(star)

    @lock
    def removeCatalog(self, catalog):
        catalog = self._getCatalog(catalog)
        session.delete(catalog)
        session.commit()


    @lock
    def removeCatalogStar(self, star, catalog):
        star = self._getStar(star)
        catalog = self._getCatalog(catalog)
        star.catalogs.remove(catalog)
        session.commit()


    @lock
    def removeExposureNote(self, exposure, note):
        exposure = self._getExposure(exposure)
        try:
            note = ExposureTask.query.filter_by(exposure = exposure).filter_by(name = note).one()
            exposure.tasks.remove(note)
            session.delete(note)
            session.commit()
        except: raise Exception("Note not found: %s in %s" % (note, exposure))


    @lock
    def removeExposureStar(self, exposure, star):
        star = self._getExposureStar(exposure, star)
        session.delete(star)
        session.commit()


    @lock
    def removeStarMagnitude(self, star, filter, source = None):
        star = self._getStar(star)
        q = StarMagnitude.query.filter_by(star = star).filter_by(filter = filter)
        if source != None:
            source = self._getCatalog(source)
            q = q.filter_by(source = source)
        try:
            mag = q.one()
            session.delete(mag)
            session.commit()
        except: raise Exception("Star magnitude data not found: %s in %s" % (filter, star))


    @lock
    def restore(self, filename):
        """
        Restores the database contents from a backup file.
        """
        # Note: restore function calls commits to the database itself (several times)
        restore(self, filename)


    @lock
    def updateExposure(self, oldExposure, newExposure, task = None):
        """
        Updates an exposure in the database, adding the attached exposure-stars
        within the exposure, if there are any. A task done to the object can
        also be optionally specified as a string or a tuple of the form (task,
        details, date), with the date optional.
        """
        if isinstance(newExposure, Image):
            image = newExposure
            filename = image.compressedFilename()
        elif isinstance(newExposure, StringTypes):
            image = Image.fromFile(newExposure)
            filename = newExposure
        else:
            try:
                image = Image.fromFile(newExposure.getFileName())
                filename = newExposure.getFileName()
            except AttributeError:
                raise ValueError("Data cannot be passed in using this type of object.")

        # Get latitude from FITS header or site information
        if 'LATITUDE' in image:  latitude = Coord.fromDMS(image['LATITUDE'])
        elif 'SITELAT' in image: latitude = Coord.fromD(image['SITELAT'])
        else:                    latitude = None

        # Get longitude from FITS header or site information
        if 'LONGITUD' in image:   longitude = Coord.fromDMS(image['LONGITUD'])
        elif 'SITELONG' in image: longitude = Coord.fromD(image['SITELONG'])
        else:                     longitude = None

        # Get observation date and time from FITS header
        try:    obsdate = datetime.strptime(image['DATE-OBS'], '%Y-%m-%dT%H:%M:%S')
        except: obsdate = None

        # Calculate local sidereal time
        if not isinstance(latitude, NoneType) and not isinstance(longitude, NoneType):
            obs = ephem.Observer()
            obs.lat = latitude.strfcoord('%(d)d:%(m)d:%(s).2f')
            obs.long = longitude.strfcoord('%(d)d:%(m)d:%(s).2f')
            obs.pressure = 0
            obs.date = obsdate
            lst = Coord.fromHMS(str(obs.sidereal_time()))
        else:
            lst = None

        # Get RA and Dec from FITS header
        ra = Coord.fromD(image['CRVAL1']); dec = Coord.fromD(image['CRVAL2'])

        # Get altitude and azimuth from FITS header or recalculate
        if ('ALT' in image) and ('AZ' in image):
            alt = Coord.fromDMS(image['ALT']); az = Coord.fromDMS(image['AZ'])
        elif isinstance(lst, NoneType):
            raDec = Position((ra, dec))
            altAz = Position.raDecToAltAz(raDec, latitude, lst)
            alt = altAz.alt; az = altAz.az
        else: alt = None; az = None

        # Get image type from FITS header, if available
        if 'IMAGETYP' in image: imagetype = image['IMAGETYP']
        else:                   imagetype = None

        # Get exposure time from FITS header, if available
        if 'EXPTIME' in image:  exptime = float(image['EXPTIME'])
        else:                   exptime = None

        # Get filter from FITS header, if available
        if 'FILTER' in image:    filter = image['FILTER']
        elif 'FILTERS' in image: filter = image['FILTERS']
        else:                    filter = None

        # Get observer from FITS header, if available
        if 'OBSERVER' in image: observer = image['OBSERVER']
        else:                   observer = None

        # Set available values in exposure
        exp = self._getExposure(oldExposure)
        exp.filename = filename
        exp.ra = ra.D
        exp.dec = dec.D
        if not isinstance(alt, NoneType): exp.alt = alt.D; exp.az = az.D
        if not isinstance(lst, NoneType): exp.lst = lst.D
        if not isinstance(latitude, NoneType): exp.latitude = latitude.D
        if not isinstance(longitude, NoneType): exp.longitude = longitude.D
        exp.obsdate = obsdate
        if imagetype != None: exp.imagetype = imagetype
        if exptime != None: exp.exptime = exptime
        if filter != None: exp.filter = filter
        if observer != None: exp.observer = observer

        if task != None:
            if not isinstance(task, tuple): self._addExposureNote(exp, task)
            elif len(task) > 2:             self._addExposureNote(exp, task[0], task[1], task[2])
            else:                           self._addExposureNote(exp, task[0], task[1])

        try: self._addExposureStars(exp, newExposure.getAllStars())
        except Exception, e: self.log.debug("Cannot add exposure stars: %s\n%s" % (exp, e))

        self.exposureUpdated(exp)
        session.commit()
